﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using elibrary.Classes;
using elibrary.Models.Managers;

namespace elibrary.Models.Entities
{
    public class UserOrder
    {
        /* Class Fields [Start] ----------------------------------------------------------------- */
		[Key]
		public int id { get; set; }
		public int buyerId { get; set; }
		public DateTime orderDate { get; set; }
		public string deliveryAdd { get; set; }
		public UserOrderStatus status { get; set; }
		/* Class Fields [End] ------------------------------------------------------------------- */

		/* Computed Fields [Start] -------------------------------------------------------------- */
		[NotMapped]
		public List<UserOrderItem> orderItems
		{
			get { return this.getOrderItems(this.id); }
			set { this.setOrderItems(value.ToArray()); }
		}

		public int totalItems
		{
			get { return this.orderItems.Count; }
		}

		public bool hasItems
		{
			get { return this.totalItems > 0; }
		}

		public decimal orderCost
		{
			get { return this.getOrderCost(); }
		}

		[NotMapped]
		public User buyer
		{
			get { return this.getBuyer(this.buyerId); }
			set { this.buyerId = value.id; }
		}

		[NotMapped]
		public string addressStreet
		{
			get { return Utilities.pieceString(this.deliveryAdd,'$',0,""); }
			set { this.deliveryAdd = Utilities.setPieceString(this.deliveryAdd,'$',0,value); }
		}

		[NotMapped]
		public string addressExtra
		{
			get { return Utilities.pieceString(this.deliveryAdd,'$',1,""); }
			set { this.deliveryAdd = Utilities.setPieceString(this.deliveryAdd,'$',1,value); }
		}

		[NotMapped]
		public string addressCity
		{
			get { return Utilities.pieceString(this.deliveryAdd,'$',2,""); }
			set { this.deliveryAdd = Utilities.setPieceString(this.deliveryAdd,'$',2,value); }
		}

		[NotMapped]
		public string addressPostcode
		{
			get { return Utilities.pieceString(this.deliveryAdd,'$',3,""); }
			set { this.deliveryAdd = Utilities.setPieceString(this.deliveryAdd,'$',3,value); }
		}

		[NotMapped]
		public string addressState
		{
			get { return Utilities.pieceString(this.deliveryAdd,'$',4,""); }
			set { this.deliveryAdd = Utilities.setPieceString(this.deliveryAdd,'$',4,value); }
		}

		[NotMapped]
		public string addressCountry
		{
			get { return Utilities.pieceString(this.deliveryAdd,'$',5,""); }
			set { this.deliveryAdd = Utilities.setPieceString(this.deliveryAdd,'$',5,value); }
		}

		public bool isCart
		{
			get { return this.status == UserOrderStatus.Cart; }
		}

		public bool isPending
		{
			get { return this.status == UserOrderStatus.Pending; }
		}

		public bool isCompleted
		{
			get { return this.status == UserOrderStatus.Completed; }
		}

		public bool isCancelled
		{
			get { return this.status == UserOrderStatus.Cancelled; }
		}

		public bool isExpired
		{
			get { return this.status == UserOrderStatus.Expired; }
		}

		public bool isDeleted
		{
			get { return this.status == UserOrderStatus.Deleted; }
		}

		public DateTime cartExpiryDate
		{
			get { return this.orderDate.AddDays(AppGlobals.glbs<int>("cart_expiry_days",2)); }
		}

		public bool isCartExpired
		{
			get { return DateTime.Now >= this.cartExpiryDate; }
		}

		public List<Payment> payments
		{
			get { return this.getAllPayments(this.id); }
		}

		public Payment pendingPayment
		{
			get { return this.getPendingPayment(this.id); }
		}

		public Payment approvedPayment
		{
			get { return this.getApprovedPayment(this.id); }
		}

		public List<Payment> rejectedPayments
		{
			get { return this.getRejectedPayments(this.id); }
		}

		public Payment latestRejectedPayment
		{
			get { return this.getLatestRejectedPayment(this.id); }
		}

		public Payment latestPayment
		{
			get { return this.getLatestPayment(this.id); }
		}

		public bool hasPayment
		{
			get { return this.payments.Count > 0; }
		}

		public Dictionary<string,object> clientHistoricalObject
		{
			get { return this.toClientHistoricalObject(); }
		}

		public Dictionary<string,object> clientManagerObject
		{
			get { return this.toClientManagerObject(); }
		}

		public Dictionary<string,object> clientRichObject
		{
			get { return this.toClientRichObject(); }
		}
		/* Computed Fields [End] ---------------------------------------------------------------- */

		/* Class Enums [Start] ------------------------------------------------------------------ */
		public enum UserOrderStatus { Cart = 0, Pending, Completed, Cancelled, Expired, Deleted }
		/* Class Enums [End] -------------------------------------------------------------------- */

		public static string describeStatus(UserOrderStatus s)
		{
			switch (s)
			{
				case UserOrderStatus.Cart:
					return "Cart";
				case UserOrderStatus.Pending:
					return "Pending";
				case UserOrderStatus.Completed:
					return "Completed";
				case UserOrderStatus.Cancelled:
					return "Cancelled";
				case UserOrderStatus.Expired:
					return "Expired";
				case UserOrderStatus.Deleted:
					return "Deleted";
				default:
					return "-";
			}
		}
		
		public bool sealCart()
		{
			if (!this.isCart || this.isCartExpired || this.totalItems < 1)
				return false;

			this.status = UserOrderStatus.Pending;

			return true;
		}

		public bool completeThis(bool forced = false)
		{
			if (forced || this.latestPayment.isApproved)
			{
				this.status = UserOrderStatus.Completed;
				Notice.notifyOrderConfirmed(this.buyerId,this.id);

				return true;
			}

			return false;
		}

		public bool expireThis()
		{
			if (!this.isCart)
				return false;
			
			if (!this.isExpired)
			{	
				this.remitOrderItems(this.orderItems);
				this.status = UserOrderStatus.Expired;
				Notice.notifyCartExpired(this.buyerId,this.id);
			}

			return true;
		}

		public bool cancelThis()
		{
			if (this.isCancelled)
				return true;

			if (!this.isPending)
				return false;

			this.remitOrderItems(this.orderItems);
			this.status = UserOrderStatus.Cancelled;

			return true;
		}

		public bool deleteThis()
		{
			if (this.isDeleted)
				return true;

			if (this.isCart || this.isPending)
				this.remitOrderItems(this.orderItems);

			this.status = UserOrderStatus.Deleted;

			return true;
		}

		// For all add and delete methods of the repository/manager classes,
		// the methods automatically commit any changes made on the business
		// entities. Otherwise any updates done on business entitis requires
		// the user to manually commit changes through the DataContext class.
		public void addOrderItem(int bid)
		{
			UserOrderItemManager itemManager = new UserOrderItemManager(AppGlobals.dataContext);
			BookManager bookManager = new BookManager(AppGlobals.dataContext);
			Book book = bookManager.get(bid);

			if (book == null)
				return;

			UserOrderItem item = new UserOrderItem { orderId = this.id, bookId = bid };
			itemManager.add(item);
			
			if (book.isSecondhand)
			{
				book.sellThis();
				// for consistency purpose
				AppGlobals.dataContext.SaveChanges();
			}
		}

		public void updateOrderItem(int oid,int bid)
		{
			UserOrderItemManager itemManager = new UserOrderItemManager(AppGlobals.dataContext);
			BookManager bookManager = new BookManager(AppGlobals.dataContext);

			UserOrderItem item = itemManager.get(oid);
            if (item == null)
                return;

			Book oldBook = item.book;
			Book newBook = bookManager.get(bid);
			if (oldBook.id == bid || newBook == null)
				return;

			item.bookId = bid;
			newBook.sellThis();
			oldBook.remitThis();
			
			AppGlobals.dataContext.SaveChanges();
		}

		public void deleteOrderItem(int oid)
		{
			UserOrderItemManager itemManager = new UserOrderItemManager(AppGlobals.dataContext);
			UserOrderItem item = itemManager.get(oid);

			if (item == null)
				return;

			item.book.remitThis();
			itemManager.delete(oid);
		}

		public void clearOrderItems()
		{
			UserOrderItemManager itemManager = new UserOrderItemManager(AppGlobals.dataContext);

			// instead of using remitOrderItems, a similar one
			// is rewritten for the sake of performance i.e.
			// saving one loop.
			foreach (var item in this.orderItems)
			{
				item.book.remitThis();
				itemManager.delete(item,false);
			}

			// changes are only committed after all operations although
			// repository classes would otherwise commit changes
			// for add and delete methods. (here it is overriden)
			AppGlobals.dataContext.SaveChanges();
		}

		private void remitOrderItems(List<UserOrderItem> items)
		{
			if (items == null)
				return;

			foreach(var item in items)
				item.book.remitThis();

			AppGlobals.dataContext.SaveChanges();
		}

		private List<UserOrderItem> getOrderItems(int oid)
		{
			UserOrderItemManager itemManager = new UserOrderItemManager(AppGlobals.dataContext);
			
			return itemManager.getAllByOrder(oid);
		}

		// use this only if you are sure what you are doing
		private void setOrderItems(UserOrderItem[] items)
		{
			UserOrderItemManager itemManager = new UserOrderItemManager(AppGlobals.dataContext);
			List<UserOrderItem> current = this.orderItems;

			// considering refactoring this code
			foreach (var item in current)
			{
				bool found = false;

				foreach (var inner in items)
				{
					if (inner.id == item.id)
					{
						found = true;
						break;
					}
				}

				if (!found)
					itemManager.delete(item,false);
			}

			// refactor this too!
			foreach (var item in items)
			{
				bool found = false;

				foreach (var inner in current)
				{
					if (inner.id == item.id)
					{
						found = true;
						break;
					}
				}

				if (!found)
					itemManager.add(item,false);
			}

			AppGlobals.dataContext.SaveChanges();
		}

		private User getBuyer(int uid)
		{
			UserManager userManager = new UserManager(AppGlobals.dataContext);

			return userManager.get(uid);
		}

		private List<Payment> getAllPayments(int oid)
		{
			PaymentManager paymentManager = new PaymentManager(AppGlobals.dataContext);

			return paymentManager.getAllByOrder(oid);
		}

		/*
		 * Each order only has 1 pending payment hence this is the latest
		 */
		private Payment getPendingPayment(int oid)
		{
			PaymentManager paymentManager = new PaymentManager(AppGlobals.dataContext);

			return paymentManager.getByOrderStatus(oid,Payment.PaymentStatus.Pending);
		}

		/*
	     * Each order only has 1 approved payment hence this also is the latest
	     */
		private Payment getApprovedPayment(int oid)
		{
			PaymentManager paymentManager = new PaymentManager(AppGlobals.dataContext);

			return paymentManager.getByOrderStatus(oid,Payment.PaymentStatus.Approved);
		}

		private List<Payment> getRejectedPayments(int oid)
		{
			PaymentManager paymentManager = new PaymentManager(AppGlobals.dataContext);

			return paymentManager.getAllByOrderStatus(oid,Payment.PaymentStatus.Rejected);
		}

		private Payment getLatestRejectedPayment(int oid)
		{
			List<Payment> rejecteds = this.rejectedPayments;
			if (rejecteds == null || !rejecteds.Any())
				return null;

			return rejecteds.OrderBy(p => p.paymentDate).Last();
		}

		private Payment getLatestPayment(int oid)
		{
			List<Payment> all = this.payments;

			if (all == null || !all.Any())
				return null;

			return all.OrderBy(p => p.paymentDate).Last();
		}

		private decimal getOrderCost()
		{
			List<UserOrderItem> orderItems = this.orderItems;
			decimal total = 0.0m;

			if (orderItems != null)
			{
				foreach(var item in orderItems)
					total += item.cost;
			}

			return total;
		}

		private Dictionary<string,object> toClientHistoricalObject()
		{
			Dictionary<string,object> obj = Utilities.getClientRichObject(new string[]{
				"id","orderDate","totalItems"}, this);

			obj["totalAmount"] = "RM " + 
				this.orderCost.ToString("N", CultureInfo.InvariantCulture);
			obj["status"] = describeStatus(this.status);

			return obj;
		}

		private Dictionary<string,object> toClientManagerObject()
		{
			Dictionary<string,object> obj = Utilities.getClientRichObject(new string[]{
				"id","orderDate","totalItems","deliveryAdd"}, this);

			obj["user"] = this.buyerId + " - " + this.buyer.loginId;
			obj["status"] = (int)this.status;
			obj["totalAmount"] = "RM " + 
				this.orderCost.ToString("N", CultureInfo.InvariantCulture);

			return obj;
		}

		private Dictionary<string,object> toClientRichObject()
		{
			Dictionary<string,object> obj = Utilities.getClientRichObject(new string[]{
				"id","orderDate","deliveryAdd"}, this);

			obj["status"] = (int)this.status;
			obj["total"] = "RM " + 
				this.orderCost.ToString("N", CultureInfo.InvariantCulture);
			obj["totalNF"] = this.orderCost;
			obj["hasPayment"] = this.hasPayment;

			List<Dictionary<string,object>> ordered = new List<Dictionary<string,object>>();
			List<UserOrderItem> items = this.orderItems;
			foreach (var item in items)
			{
				Dictionary<string,object> oi = new Dictionary<string,object>();

				oi["id"] = item.id;
				oi["bookId"] = item.bookId;
				oi["isbn"] = item.book.isbn;
				oi["title"] = item.book.title;
				oi["type"] = (int)item.book.type;
				oi["price"] = item.cost;

				ordered.Add(oi);
			}

			obj["items"] = ordered;

			return obj;
		}		
    }
}